/*
 * This file is part of FauXML.
 * FauXML is free software: you can redistribute it
 * and/or modify it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 * 
 * FauXML is distributed in the hope that it will be
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with FauXML.
 * If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.fauxml.parser;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Stack;

/**
 * This class is a small, quick, simple XML parser. It is not meant to conform
 * to all XML standards or support all XML features, but just to be able to
 * work quickly to parse normal XML elements.
 * 
 * @author Andrew Matta
 */
public final class FauXMLParserEngine {
	private static final char[] CDATA_BEGIN_SEQUENCE = {'[', 'C', 'D', 'A', 'T', 'A', '['};

	private static final int DEFAULT_BUFFER_SIZE = 1024 * 512;	//512KB
	private static final int END_BUFFER_SIZE = 3;	//512KB
	/*private static final int NUM_IO_RETRIES = 3;
	private static final int RETRY_PAUSE_DURATION_MS = 500;*/

	public static enum Error {
		PARSER, IO, SYNTAX
	};

	public static enum State {
		NONE, CDATA, FOUND_LT, FOUND_LT_BANG, FOUND_LT_Q,
		FOUND_LT_SLASH, READ_TAG_NAME, READ_LT_BANG_NAME,
		READ_CDATA_BEGIN, FOUND_CR
	};

	private FauXMLParserEngine() {
		throw new AssertionError("FauXMLParserEngine should not be instantiated");
	}

	public static final boolean parse(InputStream xmlInputStream, FauXMLParser parser)
			throws FauXMLParseException {
		return parse(new InputStreamReader(xmlInputStream), parser, true, DEFAULT_BUFFER_SIZE);
	}

	public static final boolean parse(Reader xmlInputReader, FauXMLParser parser)
			throws FauXMLParseException {
		return parse(xmlInputReader, parser, true, DEFAULT_BUFFER_SIZE);
	}

	public static final boolean parse(InputStream xmlInputStream, FauXMLParser parser, final boolean validateTagOrdering, final int bufferSize)
			throws FauXMLParseException {
		return parse(new InputStreamReader(xmlInputStream), parser, validateTagOrdering, bufferSize);
	}

	public static final boolean parse(Reader xmlInputReader, FauXMLParser parser, final boolean validateTagOrdering, final int bufferSize)
			throws FauXMLParseException {
		char[] buffer = new char[bufferSize];
		int bufferLength;	//Stores how many bytes were read by the read() calls
		int knownTokenLengthCount = 0;	//Used by various States to track how much has been found of a needed token
		StringBuffer tokenBuffer = new StringBuffer();
		Stack stTags = null;
		if(validateTagOrdering)
			stTags = new Stack();
		//int tries;
		int lineNumber = 0;
		//Start at zero since the increment will make it 1 before acting on the first column
		int columnNumber = 0;

		int tokenStart;
		int tokenEnd;

		State state = State.NONE;

		boolean done = false;
		BufferedReader bufferedReader = new BufferedReader(xmlInputReader);
		char curChar;

		while (!done) {
			try {
				bufferLength = bufferedReader.read(buffer);
				//If this is the first line, make it line 1, since
				//non-programmers tend to think things start at 1 rather than 0.
				//We had it at zero before so that an initial IOException would
				//show an error on line 0.
				if (lineNumber == 0) {
					++lineNumber;
				}
			} catch (IOException ioe) {
				throw new FauXMLParseException(lineNumber, columnNumber, "Could not read the input XML", ioe);
			}

			tokenStart = 0;
			tokenEnd = 0;

			//Parse
			for (int bufferIndex = 0; bufferIndex < bufferLength; ++bufferIndex) {
				curChar = buffer[bufferIndex];

				//Keep line number and column number accurate
				if (curChar == '\n' || curChar == '\r') {
					++lineNumber;
					columnNumber = 0;	//Start at zero so the upcoming increment will make it column 1
				}
				//Increment column number
				++columnNumber;

				switch (state) {
					//COMPLETE
					//If we're in a CDATA section, we ignore everything but "]]>"
					case CDATA:
						//We are looking for "]]>" to terminate the CDATA

						//If our count is 0 or 1, we need ']'
						if (curChar == ']' && knownTokenLengthCount < 2) {
							++knownTokenLengthCount;
						} //If our count is length 2, we found "]]" and now need '>'
						else if (curChar == '>' && knownTokenLengthCount == 2) {
							state = State.NONE;	//We're out of CDATA now
							knownTokenLengthCount = 0;	//Reset terminator tracker
						} else {
							knownTokenLengthCount = 0;	//Reset terminator tracker
						}
						break;

					//COMPLETE
					case FOUND_LT:
						//When we find '<', we expect one of the following:
						//<!TOKEN attribute="value"!>
						//<?Directive attribute="value"?>
						//<![CDATA[ ... ]]>
						//<TAG attributes="values">
						//</TAG>
						if (curChar == '!') {
							state = State.FOUND_LT_BANG;
						} else if (curChar == '?') {
							state = State.FOUND_LT_Q;
						} else if (curChar == '/') {
							state = State.FOUND_LT_SLASH;
						} else {
							//We're reading a tag name now
							boolean success = readNameToken(tokenBuffer, curChar);
							if (!success) {
								//Illegal character found in tag name
								throw new FauXMLParseException(lineNumber, columnNumber,
										"Illegal character '" + curChar + "' found starting tag name");
							}
							state = State.READ_TAG_NAME;
						}

						break;

					//COMPLETE
					case FOUND_LT_BANG:
						//When we find "<!", we expect one of the following:
						//<!TOKEN attribute="value"!>
						//<![CDATA[ ... ]]>
						
						if(curChar == '[') {
							knownTokenLengthCount = 1;
							state = State.READ_CDATA_BEGIN;
						} else {
							boolean success = readNameToken(tokenBuffer, curChar);
							if (!success) {
								//Illegal character found in name
								throw new FauXMLParseException(lineNumber, columnNumber,
										"Illegal character '" + curChar + "' found starting <! tag name");
							}
							state = State.READ_LT_BANG_NAME;
						}

						break;

					case READ_CDATA_BEGIN:
						if(curChar == CDATA_BEGIN_SEQUENCE[knownTokenLengthCount])
							++knownTokenLengthCount;
						else {
							
						}
						if(knownTokenLengthCount == 7)	//We've found "[CDATA[" in order
							state = State.CDATA;
						break;

					case READ_LT_BANG_NAME:
//						if (Character.isWhitespace(curChar)) {*********************************************************************
//							state = State.AWAITING_LT_BANG_ATTRIBUTE;
//						} else if(curChar{
//							break;
//						}

					case NONE:
						if (curChar == '<') {
							state = State.FOUND_LT;
							continue;
						}

						break;

					default:
						//Illegal character found in name
						throw new FauXMLParseException(lineNumber, columnNumber, "Illegal parser state");
				}
			}
		}

		return true;
	}

	/**
	 * Called by the main parse function when a tag name is to be parsed.
	 * A character is accepted if it is alphanumeric, and rejected otherwise.
	 * 
	 * @param tokenBuffer A StringBuffer holding what's been read of the token so far
	 * @param curChar The next character to try to put into the buffer
	 * @return True if the character was added to the buffer, and false otherwise
	 */
	private static final boolean readNameToken(StringBuffer tokenBuffer, char curChar) {
		boolean add = false;

		if (tokenBuffer.length() == 0) {
			if (isNameFirstChar(curChar)) {
				add = true;
			}
		} else if (isNameChar(curChar)) {
			add = true;
		}

		if (add) {
			tokenBuffer.append(curChar);
		}

		return add;
	}

	/**
	 * Returns whether or not the given character can be used in a tag name
	 * (not for the first character in the tag, but for the rest)
	 * 
	 * @param curChar The character to test
	 * @return True iff curChar can be used in a tag name
	 */
	private static final boolean isNameChar(char curChar) {
		return Character.isLetterOrDigit(curChar) || curChar == '.' || curChar == '-' || curChar == '_' || curChar == ':' || isCombiningChar(curChar) || isExtender(curChar);
	}

	/**
	 * Returns whether or not the given character can be used as the first
	 * character in a tag.
	 * 
	 * @param curChar The character to test
	 * @return True iff curChar can be used in a tag name
	 */
	private static final boolean isNameFirstChar(char curChar) {
		return Character.isLetter(curChar) || curChar == '_' || curChar == ':';
	}

	/**
	 * Returns whether or not the given character is a "combining character" as
	 * specified by the XML 1.0 specification
	 * 
	 * @param curChar The character to test
	 * @return True iff curChar is a "combining character"
	 */
	private static final boolean isCombiningChar(char curChar) {
		return false;	//TODO: Make this work
	}

	/**
	 * Returns whether or not the given character is an "extender" as specified
	 * by the XML 1.0 specification.
	 * 
	 * @param curChar The character to test
	 * @return True iff curChar is an "extender"
	 */
	private static final boolean isExtender(char curChar) {
		return false;	//TODO: Make this work
	}
	//ENTITIES TO REPLACE
	//&amp;		->		&
	//&lt;		->		<
	//&gt;		->		>
	//&apos;	->		'
	//&quot;	->		"
	//&#nnn;	->		Decimal(nnn) to Unicode value
	//&#xhhh;	->		Hex(hhh) to Unicode value
	//<![CDATA[ ... ]]>
	//<!NameHere key="val" !>
}
